Skip to content

Fix RTL audit findings across utility modules - #4

Merged
aca-logimentor merged 35 commits into
mainfrom
bugfix/rtl-audit-fixes
Oct 18, 2026
Merged

aca-logimentor merged 35 commits into
mainfrom
bugfix/rtl-audit-fixes

Conversation

@aca-logimentor

@aca-logimentor aca-logimentor commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes functional bugs found during a code review of the library, hardens the
CDC synchronizers for synthesis, verifies them with vendor synthesis, and
closes the coverage gaps that were hiding the bugs. To be tagged v0.1.0 at
merge (see CHANGELOG).

Breaking changes

  • lm_util_bitsum: bitsum_o is f_ceil_log2(g_din_w + 1) bits wide.
  • lm_util_pulse_stretch: input pulse active high; stretch adds exactly
    g_pulse_overlength cycles and is retriggerable.
  • lm_util_counter: down direction counts g_wd_timer-1 .. 0.
  • lm_util_delay_pulse, lm_util_delay_var: leading-edge trigger, inactive
    reset level; lm_util_delay_var with g_delay_max = 1 and C_LM_PULSE
    follows the pulse architecture; any g_arch_type other than C_LM_SRL or
    C_LM_PULSE is rejected at elaboration for every g_delay_max.
  • lm_util_ccd_resync (g_meta_levels < 2) and lm_util_ccd_sync_pulse
    (g_delay_len < 2) stop elaboration with severity failure.
  • lm_util_clock_measure: after a reset the first value is published only
    when a complete window has been measured (second tick after the release);
    the partial first window is no longer published.
  • lm_util_pkg: corrected results of f_div_ceil_2pwr, f_smallest,
    f_is_power_of_two, f_string_substr; f_string_format rounds the exact
    binary value of the real, exact halves away from zero (9.9995 with
    precision 3 gives "9.999", the real being 9.99949999999999938893...),
    has no decimal point with precision 0, and an integer part above
    integer'high or a precision above 9 stops with a failure, checked
    before any intermediate computation (see CHANGELOG). The exact rounding
    assumes IEEE 754 binary64 reals with every operation rounded on its own;
    verified with GHDL 6.0.0 (LLVM), not with Vivado Simulator or
    Questa/ModelSim.

RTL fixes

  • lm_util_clock_measure: the toggle synchronizer's third stage sampled the
    first stage and the edge detector compared against it; the reported
    frequency missed the window-closing cycle. A reset no longer publishes a
    stale value: a pending measurement, the ready transition forced by the
    measured-domain reset (also after the release with a slow or stopped
    measured clock) and pre-reset values in the synchronizer stages are ignored
    until the stages have been reloaded after the release. The synchronizer
    stages carry no reset logic (Quartus identified a one-stage chain when they
    did).
  • lm_util_bitsum: the all-ones count wrapped to zero for power-of-two
    input widths.
  • lm_util_pulse_stretch: unit lengths, stretch length and retrigger.
  • lm_util_counter: the down direction underflowed from 0 through the full
    register range; the watchdog range check no longer overflows integer.
  • lm_util_delay_pulse / lm_util_delay_var: wide pulses were delayed from
    their trailing edge; reset drove the active level for low-polarity pulses;
    dv_o was undriven in two configurations; unit delay in pulse mode.
  • lm_util_ccd_sync_bus: a request held across the reset release is not a
    new request, and a request rising right after the release is no longer
    dropped.
  • lm_util_clock_mux: Quartus synthesis failed on an illegal
    altera_attribute "-name KEEP ON"; Quartus keeps the gated clocks through
    syn_keep.
  • lm_util_pkg: f_smallest, f_div_ceil_2pwr, f_div_ceil(time,time),
    f_is_power_of_two, f_string_format (rounding carry, values up to
    integer'high without overflow, no second rounding when the scaled
    fraction lands on a decimal half), f_string_substr, f_vector_tree.

Synthesis hardening and constraints

  • Synchronizer chains carry async_reg/shreg_extract (Vivado),
    altera_attribute synchronizer identification (Quartus) and
    syn_preserve/syn_srlstyle (Synplify Pro / Lattice LSE, not verified;
    stated next to the attributes and in each header).
  • Each CDC module header lists Vivado and Quartus constraint examples that
    select registers and pins by internal signal names only; the user guide
    explains them and documents the asynchronous clock groups alternative with
    the behavior observed on Vivado 2023.1.
  • tools/synth/run_vendor_synth.py checks the synchronizer structure on the
    Vivado netlist and the Quartus reports, loads every published Vivado and
    Quartus constraint example, and has negative cases for each check. A check
    that passes when something is absent first requires an open, non-empty
    netlist or a complete, recognized report, otherwise it fails with an
    LM_ERROR. A Quartus table panel that a check reads must be complete when
    present (column header, data rows, whole-line borders aligned with the
    rows, closing border). Only the register packing panel may be absent; a
    valid report without it still passes. The check detects damaged or unrecognized reports,
    not reports edited to stay well formed (see tools/synth/README.md).
  • lm_util_async_reset, lm_util_clock_measure: the false path on the
    CLR/clrn pins removes the reset on those pins from timing analysis,
    recovery/removal included; the synchronized release and its downstream use
    stay with the integrator.
  • CI installs GHDL 6.0.0 (LLVM) with ghdl/setup-ghdl pinned by SHA; VUnit
    is pinned to 4.7.1; invalid generics are checked by
    sim/scripts/check_elab_failures.py.

Verification

Runs on the final branch head unless noted.

  • GHDL 6.0.0 (LLVM), --std=08, fast regression: 261/261 (local and CI)
  • GHDL 6.0.0 (LLVM), --std=08, full regression: 390/390
  • Fast regression on every commit of the branch: all passed
  • Negative elaboration checks: 23/23 (local and CI), including
    f_string_format inputs of +-1.0e300 and carries just above
    integer'high; 10 consecutive local runs after the fix of an
    intermittent link failure on Windows: 10/10
  • f_string_format against exact reference vectors (Python decimal,
    fixed seed): 0 mismatches in 1500 (fast) and 20000 (full) vectors; the
    body before the double-rounding fix, the body of the first
    lm_util_pkg fix of this PR and the body on main (5d53232) fail
    393, 8902 and 2708 of the 20000
  • CI: GHDL 6.0.0 (LLVM) from ghdl/setup-ghdl@75eff367 (v1.5.1),
    VUnit 4.7.1, both jobs green
  • lm_util_clock_measure reset tests: no publication during a reset or
    before the second tick after it, and every published value is a full
    window count; reset at every phase across two windows, 1 to 5 cycle
    resets, measured clock faster, slower and stopped across the reset
  • Vivado 2023.1, xc7a35tcsg324-1: ccd_resync (2 and 4 stages),
    ccd_sync_bus, ccd_sync_pulse, async_reset (2 and 4 stages),
    clock_measure, clock_mux: every stage an FD* cell with
    ASYNC_REG = TRUE, no SRL, header constraint examples matched, listed
    by report_exceptions and not ignored. Rerun after the last change to
    the Vivado checks: clock_measure_16 (20 checks) and
    ccd_resync_2ff (8 checks) passed
  • Quartus Prime Lite 23.1std, MAX 10 10M08SAE144C8G: same modules,
    every stage a user-specified synchronizer in report_metastability, no
    shift-register inference, constraint examples matched, valid and not
    ignored. Rerun after the last change to the Quartus checks: clock_measure_16 (18 checks; three
    2-register chains, design MTBF above 1 billion years) and
    ccd_resync_2ff (8 checks) passed
  • User guide constraint example loaded on both tools
  • Negative cases fail the checks on both tools (rerun after the last
    change to each tool's checks):
    chain without attributes (Vivado packs it into SRL16E, no
    ASYNC_REG; Quartus identifies it only automatically), renamed chain
    (empty collections), 32x8 delay line (8 SRL* cells on Vivado,
    SHIFT_TAPS on Quartus); evaluated as regular cases they fail the run.
    Vivado neg_chain_renamed passed on the runner's automatic retry after
    a transient Vivado script read error
  • Report completeness: an emptied or truncated Quartus Analysis &
    Synthesis report, a packing panel gutted to its title, to its header
    or without its closing border, a packing panel
    with an invalid border and a chain Statistics panel without its title
    (Quartus), a closed Vivado
    design and an empty Vivado ignored-exceptions report each
    fail the run with LM_ERROR; before the fixes the same cases passed
    the checks. On copies of all 89 real results with structural checks,
    the final runner gives the same verdicts as the runner before the
    last change to the report checks
  • Asynchronous clock groups on Vivado 2023.1: set_max_delay -datapath_only totally overridden, set_bus_skew still reported
    (8 endpoints, 0.687 ns skew)
  • Synplify Pro / Lattice LSE and Synplify Pro for Microchip Libero
    attributes: not verified (no licensed tool available)

Known follow-ups (not in this PR)

  • lm_util_ccd_sync_bus: no deassert acknowledgment; the output domain has
    no reset.
  • Modules still without a testbench: clock_mux, lfsr, mux, mux_or,
    pri_arbiter, rr_arbiter.
  • lm_util_lfsr seed and width handling.

Merge notes

Rebase merge. The planning-only commits were dropped from the branch
before the merge, and the last commit dates the v0.1.0 changelog section.
After the rebase: fast regression 261/261, full regression 390/390,
elaboration checks 23/23, CI green.

- f_smallest(t_natural_arr) always returned 0 (accumulator started at 0)
- f_div_ceil_2pwr rounded up by unit increments capped at 64 iterations,
  returning non-power-of-two values for large ratios
- f_div_ceil(time, time) truncated sub-ns remainders when computing the mod
- f_is_power_of_two failed on 0 and relied on to_signed truncation
- f_string_format lost the carry when the fraction rounded up to 10**precision
- f_string_substr bounds check was inverted, flagging valid substrings
- the unsupported-operation assert in f_vector_tree used 'assert true' and
  could never fire; it also referenced the pre-rename package name
The third toggle synchronizer stage sampled the first stage instead of the
second, and the edge detector XORed against the first stage, which samples
the asynchronous toggle directly: a metastable event could double or drop
the measurement window. The reported frequency also missed the cycle that
closes the window (off by one), and the ready synchronizer chain was not
reset, so a reset while the toggle was high produced a spurious output
update. The testbench now checks the second, steady-state window, whose
count does not depend on the reset phase.
bitsum_o was f_ceil_log2(g_din_w) bits wide, but counting the ones of an
all-ones input yields g_din_w, which needs f_ceil_log2(g_din_w + 1) bits:
for power-of-two input widths the result wrapped to zero.
- g_pulse_overlength = 1 deasserted the output on every idle cycle because
  the counter idles at 0 and the compare was not qualified by the enable
- g_pulse_length = 1 never produced a pulse (0 < 0 is never true); the
  output is now active for the whole enable window
- stretch mode now adds exactly g_pulse_overlength cycles after the input
  falling edge (was one cycle short) and is retriggered by a new rising
  edge instead of cutting the output mid-pulse
- the input pulse is defined active high; g_out_level only sets the output
  polarity, and the zero-overlength passthrough honours it
- the fixed-length window ignores edges arriving while it is running
- the testbench drives an active-high input, checks mode-dependent widths,
  and skews scheduled transitions off the clock edges so sampling is
  unambiguous with the combinational input stage
- both pulse counters restarted on every active input cycle, so pulses
  wider than one clock were delayed from their trailing edge; they now
  trigger on the leading edge only
- reset drove the outputs to '0', which is the active level when the
  configured pulse polarity is low; reset now drives the inactive level
- delay_var: dv_o was never driven for g_delay_max = 1 and in the pulse
  architecture; a runtime delay of 1 now works in pulse mode; unsupported
  g_arch_type values (including the never-implemented mem architecture)
  are rejected at elaboration; superseded commented-out code removed
- the delay_var testbench drove the DUT output dv_o instead of dv_i and
  never checked dv_o; both fixed
Counting down, the counter started from 0 and underflowed through the full
register range before ever reaching the watchdog compare. The down
direction is now symmetric to up: reset loads g_wd_timer-1, the counter
runs down to 0, pulses timer_o and reloads. g_wd_timer is validated
against the counter width at elaboration. The testbench load test carried
its stimulus commented out and never drove load_i; it now loads a value
and checks that counting resumes from it in both directions.
Add async_reg/shreg_extract attributes to the hand-rolled synchronizer
chains (ccd_resync, ccd_sync_pulse, async_reset, clock_measure via its own
commit, clock_mux) so synthesis keeps the flops discrete and adjacent
instead of mapping them to SRL primitives. The clock_mux anti-glitch keep
attribute was boolean-typed, which Vivado silently ignores; it is now a
string. ccd_resync's meta-levels guard is a hard failure like the other
guards. ccd_sync_pulse validates g_delay_len. ccd_sync_bus parks its
request edge detector high across reset so a request held asserted through
the reset release is not seen as a new rising edge.
- counter: down direction and load tests enabled, watchdog and load tests
  get explicit configurations
- pulse_stretch: active-low output and unit lengths enabled
- delay_pulse: multi-cycle input pulses enabled
- delay_var: pulse delay of 1 and g_delay_max = 1 enabled
- bitsum: power-of-two input widths exercise the all-ones count
- document the removed mem architecture in the user guide and record the
  behavioral changes in the changelog
async_reg/shreg_extract are Xilinx-only and are silently ignored by other
tools, so the synchronizer chains now also carry syn_preserve/syn_srlstyle
for Synplify Pro and Lattice LSE, and altera_attribute synchronizer
identification (plus shift-register recognition off) for Intel Quartus.
The clock_mux keep intent is expressed per tool: keep as a string for
Vivado, syn_keep for Synplify/LSE, and KEEP ON via altera_attribute for
Quartus, restoring the Quartus behavior lost when the boolean keep was
converted to a string.

Attributes only prevent structural mangling; the crossing paths still need
project-level timing constraints, so each module header now carries a
ready-to-adapt set_false_path/set_max_delay example per tool and the user
guide gains a CDC attributes and timing constraints section.
@aca-logimentor
aca-logimentor marked this pull request as ready for review July 6, 2026 10:15
Copilot AI review requested due to automatic review settings July 6, 2026 10:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses internal RTL audit findings across multiple lm_util utility blocks by fixing functional edge cases, hardening CDC/synchronizer structures for synthesis, and re-enabling/expanding VUnit regression coverage for previously failing configurations.

Changes:

  • Fixes functional corner cases in pulse stretching, variable/fixed pulse delaying, counting (incl. down direction), clock measurement, and several lm_util_pkg helper functions.
  • Adds vendor-specific synthesis attributes (async_reg, shreg_extract, syn_preserve, altera_attribute, etc.) to prevent SRL mapping/optimization of synchronizer flops, and documents required project-level timing constraints.
  • Expands and corrects testbenches plus sim/scripts/run.py to cover previously disabled failing parameter sets.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/lm_util_pulse_stretch.vhd Fixes fixed-length and stretch-mode corner cases; clarifies input/output polarity behavior.
src/lm_util_pkg.vhd Corrects multiple utility functions (min, ceil divisions, power-of-two check, formatting/substr bounds, assert guard).
src/lm_util_delay_var.vhd Fixes reset polarity behavior, drives dv_o in all modes, rejects unsupported architectures, hardens pulse-mode behavior.
src/lm_util_delay_pulse.vhd Triggers delay from leading edge (multi-cycle pulses), fixes reset polarity behavior.
src/lm_util_counter.vhd Makes down-count symmetric to up-count, adds generic validation, adjusts reset/reload behavior.
src/lm_util_clock_mux.vhd Fixes/extends synthesis attributes (keep, synchronizer attributes) and adds constraint guidance.
src/lm_util_clock_measure.vhd Fixes toggle synchronizer staging/edge detect, corrects off-by-one window close, clears ready chain on reset, adds CDC attributes/docs.
src/lm_util_ccd_sync_pulse.vhd Adds CDC attributes and enforces minimum synchronizer depth for metastability reduction.
src/lm_util_ccd_sync_bus.vhd Prevents false edge detect on reset release when request is held asserted.
src/lm_util_ccd_resync.vhd Adds CDC attributes and improves documentation/constraint guidance; strengthens generic check severity.
src/lm_util_bitsum.vhd Widens bitsum_o to avoid wrap on all-ones for power-of-two input widths.
src/lm_util_async_reset.vhd Adds CDC attributes and constraint guidance for async reset resynchronization.
sim/tb/tb_vu_lm_util_pulse_stretch.vhd Updates stimulus and checks for new polarity definition and corrected stretch/fixed-length semantics.
sim/tb/tb_vu_lm_util_delay_var.vhd Fixes TB driving (dv input vs output) and adds dv_o assertions.
sim/tb/tb_vu_lm_util_counter.vhd Adds down-direction and load-mode coverage; updates expectations for new reload behavior.
sim/tb/tb_vu_lm_util_clock_measure.vhd Adjusts TB to check steady-state measurement window (reset-phase independence).
sim/tb/tb_vu_lm_util_bitsum.vhd Updates TB signal width to match widened bitsum_o.
sim/scripts/run.py Re-enables/extends parameter sweeps for previously failing configs; adds new targeted coverage sets.
docs/user_guide.md Updates module descriptions and documents CDC attributes + required timing constraint patterns.
CHANGELOG.md Records breaking interface/behavior changes and summarizes fixes + test coverage improvements.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lm_util_counter.vhd Outdated
2**g_data_w overflows the 32-bit VHDL integer at elaboration for
g_data_w >= 31, failing valid configurations. Check the equivalent
f_ceil_log2(g_wd_timer) <= g_data_w instead; the boolean 'and'
short-circuit keeps f_ceil_log2 from being called when g_wd_timer < 1.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Comment thread src/lm_util_counter.vhd
Comment thread src/lm_util_delay_var.vhd Outdated
The pulse delay trigger was still level-based (input active and counter
idle), so an input held active beyond the configured delay retriggered a
new delay right after the output pulse was emitted, duplicating the event.
Both lm_util_delay_pulse and the lm_util_delay_var pulse architecture now
detect the leading edge against the previous input sample.

In the delay_var pulse architecture dv_o now pulses together with the
delayed output pulse, consistent with the SRL architecture, instead of
mirroring dv_i one cycle late. The delay_pulse unit-delay register gains
the reset-to-inactive-level behavior the other configurations already had.

Testbenches check that the output pulse is one cycle wide and that an
input wider than the delay does not retrigger; new configurations cover
g_pulse_width greater than g_delay.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Comment thread src/lm_util_counter.vhd
Comment thread src/lm_util_ccd_sync_bus.vhd
aca-logimentor added a commit that referenced this pull request Oct 17, 2026
Plan for the remaining work before merging PR #4: review thread
replies, GHDL 6.0.0 pinning in CI, structural CDC checks in vendor
synthesis, CDC constraint options, changelog and merge method, and
the phase B commit sequence with closure criteria.
aca-logimentor added a commit that referenced this pull request Oct 17, 2026
Decisions taken, the constraint options not adopted with the reason, and
the deviations from the plan. Like the plan itself, this commit is
dropped before the merge.
CI installed GHDL from the Ubuntu 24.04 archive (4.1), while the
reference simulator is GHDL 6.0.0 with the LLVM backend. Install it with
ghdl/setup-ghdl pinned to a commit SHA and fail the job when the
reported version or backend differ. Pin vunit_hdl in requirements.txt,
which both CI and local environments install from.

The user guide states the reference simulator and documents the Windows
path length failure mode of long VUnit output directories.
The g_arch_type check was skipped for g_delay_max = 1, so any value,
including the removed mem architecture, elaborated silently. The check
is now unconditional.

With g_delay_max = 1 and C_LM_PULSE the unit-delay branch was a plain
register: reset drove '0' (the active level for a low-active pulse),
dv_o mirrored dv_i, and an input held active was passed through. It now
follows the pulse architecture semantics with a delay of one: leading-edge
trigger, one-cycle output pulse with dv_o, inactive level on reset.

The delay_var testbench checks the reset level and adds a
pulse_long_input test; both run for pulse levels 0 and 1, also with
g_delay_max = 1 (four of these configurations fail on the previous RTL).
sim/scripts/check_elab_failures.py elaborates entities with invalid
generics and expects the intended assertion, covering delay_var,
counter (g_wd_timer <= 0), ccd_resync and ccd_sync_pulse, with a valid
configuration of each as control. CI runs it before the regression.
Parking the request edge detector at '1' during reset hid a request held
across the reset release, but it also dropped a request whose
resynchronized rising edge reached the input domain on the first cycle
after the release: the bus was never latched and out_rdy_o never pulsed.
The detector now loads the resynchronized request during reset, which
covers both cases.

The module had no testbench. tb_vu_lm_util_ccd_sync_bus checks a basic
two-transfer sequence, a request held across reset (no transfer) and a
request rising right after the release (one transfer), with 2 and 3
synchronizer stages and an output clock faster and slower than the
input clock. req_rises_after_reset fails on the previous RTL.
In get_registers *s_din_meta[0] and get_cells */s_din_meta[0], Tcl runs
[0] as a command ("invalid command name 0"), so the ccd_resync header
and user guide examples failed when pasted into an SDC or LDC file. The
ccd_sync_pulse examples were braced but unescaped, so Quartus treated
[0] as a character class instead of the bus index. Brace the patterns
and escape the brackets. The Vivado examples sit inside -filter {...}
and are unchanged.
The altera_attribute "-name KEEP ON" added for the gated clocks stops
Quartus Analysis & Synthesis with "Attribute name KEEP is illegal", so
lm_util_clock_mux no longer synthesized in Quartus. KEEP is not a legal
assignment name, and the Quartus keep intent is already carried by the
syn_keep attribute: Quartus 23.1 turns it into IGNORE_LCELL_BUFFERS and
REMOVE_REDUNDANT_LOGIC_CELLS off on s_gated_clks. Drop the illegal
assignment and update the attribute table of the user guide.

Found by the clock_mux_2 vendor synthesis case on Quartus Prime Lite
23.1std (MAX 10), which now passes.
f1be738 cleared the three ready resampling stages with the synchronous
reset. On MAX 10 the reset becomes logic between the stages, so Quartus
identifies a one-register synchronizer chain (worst-case MTBF about 3.8e3
years at 100 MHz instead of more than 1e9 years for the other chains).

Leave the two synchronizer stages and the edge detector stage without
reset, and block the output update while rst_n_i is low instead. The
edge detector keeps tracking the synchronized level during reset, so the
toggle transition forced by a measure-domain reset is still not taken as
a valid ready, which is what f1be738 was after.

Found by the clock_measure_16 vendor synthesis case on Quartus Prime Lite
23.1std (MAX 10), which now identifies both stages as a user-specified
synchronizer. The clock_measure regression passes.
The CDC cases only matched regex hints such as "Register|Slice" in any
log, generated scripts included, so they passed even when a chain was
packed into an SRL, and a "review" status still exited with code 0.

Cases can now declare synchronizer chains and no_shift_register:
- Vivado: a Tcl query on the synthesized netlist checks that every chain
  register is an FD* cell with ASYNC_REG = TRUE, that the expected number
  of registers exists, and that no SRL* cell was inferred;
- Quartus: the flow adds the Fitter and report_metastability (case clocks
  in a generated SDC, several clocks asynchronous); every chain register
  must be listed as a user-specified synchronizer, and the Analysis &
  Synthesis report must show no SHIFT_TAPS packing.
Each check writes one line to reports/lm_checks.txt; a failing or missing
check fails the case and the run. Regex hints only read vendor reports
and can no longer turn a case into "passed".

New cases cover ccd_resync (2 and 4 stages), ccd_sync_bus, ccd_sync_pulse,
async_reset (2 and 4 stages), clock_measure and clock_mux. Negative cases
must fail each kind of check: a chain without attributes, a chain whose
signal has another name (test-only entities under tools/synth/negative),
and a delay line packed into shift registers. --ignore-expected-failures
evaluates them as regular cases.

The reference targets are Vivado 2023.1 (xc7a35tcsg324-1) and Quartus
Prime Lite 23.1std on MAX 10 (10M08SAE144C8G), where synchronizer
identification and the MTBF report work as on Cyclone V. A Quartus
family or device, or a Vivado part, that is not installed now stops with
an LM_ERROR naming it. Vivado's intermittent "couldn't read file" failure
on Windows is retried once.
42413b0 escaped the brackets inside the braced patterns, assuming that
Quartus reads [0] there as a character class. Quartus Prime Lite 23.1
does the opposite: in the Timing Analyzer, get_registers {*s_din_meta[0]}
matches the first synchronizer stage, while {*s_din_meta\[0\]} matches
nothing and the false path is ignored for an empty collection. Braces
alone are what the examples need, since they already stop the Tcl
command substitution.

Restore the literal indices in the ccd_resync and ccd_sync_pulse headers
and in the user guide. The Lattice forms follow the same pattern and are
not verified.
The constraint examples depended on instance names (*/inst_resync*/...)
or on a hand-written port name, and nothing checked that they still
matched the RTL. Following option (a) of the completion plan:

- every CDC module header (ccd_resync, ccd_sync_pulse, async_reset,
  clock_measure, clock_mux, and new for ccd_sync_bus) lists Vivado and
  Quartus constraints that select registers or asynchronous clear pins
  only by internal signal names, with braced bus indices; the Lattice
  forms are marked as not verified;
- the user guide shows the ccd_resync example in a marked block, explains
  the bracket rule, and documents the asynchronous clock groups
  alternative with the result observed on Vivado 2023.1: the clock groups
  totally override set_max_delay -datapath_only on the ccd_sync_bus data
  bus, while set_bus_skew is still reported (8 endpoints, 0.687 ns skew);
- run_vendor_synth.py loads every Vivado and Quartus example of the
  headers and of the guide block on a timing netlist with the case clocks
  and fails on an empty collection, a Tcl error, an exception that is not
  listed by report_exceptions, an ignored or overridden exception, or (on
  Quartus) an invalid one; set_bus_skew must appear in report_bus_skew.

neg_chain_renamed now also loads the ccd_resync example on a chain named
s_sync_ff and must fail the empty-collection check, and
ccd_sync_bus_8_clock_groups (Vivado only) must show the override while
the bus skew check still passes. A case can be restricted to some tools.

The runner also skips the script lines that Vivado echoes into its log
when it extracts the failure message, and retries the intermittent
"rt-undefined" Vivado failure like the "couldn't read file" one.
CHANGELOG: the Unreleased section becomes v0.1.0, to be tagged at merge.
All behavior changes are listed as breaking: bitsum, pulse_stretch,
counter, the delay pulse trigger, delay_var unit delay in pulse mode and
the g_arch_type check, the failure severity of the ccd_resync and
ccd_sync_pulse depth guards, and the lm_util_pkg results of
f_div_ceil_2pwr, f_smallest, f_is_power_of_two and f_string_substr.
The fixes add ccd_sync_bus (request rising right after reset),
clock_measure (no reset logic between synchronizer stages) and the
invalid Tcl of the constraint examples; Changed covers the clock_mux keep
through syn_keep, the constraint examples, the synthesis checks and the
pinned simulator.

User guide: a Verified column in the CDC attribute table (Vivado 2023.1
on xc7a35tcsg324-1, Quartus Prime Lite 23.1std on MAX 10 10M08SAE144C8G;
Synplify Pro, Lattice LSE and Synplify Pro for Microchip Libero not
verified) with what the verification checks, the reference targets and
command for the CDC campaign, and the CDC row of the sensitive cases.
A reset could publish a stale measurement after the release. The
measured-domain reset forces the ready toggle to 0; when that transition
reached the reference domain after the release (single-cycle reset, slow
or stopped measured clock) the previous sample was published again. A
measurement pending at the reset and the partial first window, counted
from the reset release, were published as well.

The synchronizer stages keep no reset logic. Validity is qualified
around them instead:

- measured domain: ticks are ignored until every register of the tick
  path has been loaded after the reset release, and the first tick only
  opens the window, so a published window starts and ends on ticks
  generated after the reset;
- reference domain: ready transitions are ignored until every ready
  synchronizer stage has been loaded after the release and shows the
  measured-domain reset level '0'. A '1' there is a ready not reset yet,
  whose forced transition is not a measurement.

New testbench cases check every write of the output register, also when
the value does not change: none during reset, none before the second
tick after the release, and each one carries the count of a complete
window. They cover the reported case, a reset at every phase across two
windows, reset lengths of 1 to 5 cycles, measured clocks faster and
slower than the reference, and a measured clock stopped before, during
and after the reset.
cb93e3e rounded the whole value scaled by 10**precision and converted it
to an integer, so any value above integer'high / 10**precision wrapped
silently: f_string_format(3000000.0, 3) returned "-2147483.352" instead
of "3000000.000".

The fraction is rounded alone and a fraction that rounds up to
10**precision carries into the integer part. Inputs that cannot be
formatted stop with a failure: an integer part above integer'high (also
after the carry) and a precision above 9. The math_real reference body
returns the floor and round arguments unchanged from integer'high on, so
floor never gets a larger argument. Precision 0 gives no decimal point.

tb_vu_lm_util_pkg checks large values up to the integer limit, the carry,
negative values and precision 0. The guards are checked at elaboration by
check_elab_failures.py through a test-only entity under sim/elab.
A check that passes when something is absent took an empty or partial
input as proof of absence: with an empty project.map.rpt the Quartus
no_shift_register check found no packing panel and passed on a design
that packs a shift register. The same held for the Vivado checks on a
closed or empty netlist and on an empty report_exceptions -ignored
string, and for a check missing from lm_checks.txt.

Before such a conclusion the runner now shows that its input is
complete, otherwise the check fails with an LM_ERROR detail that fails
the case, a negative case included:

- Quartus Analysis & Synthesis report: title, summary and messages
  panels in the table of contents, every listed panel present, a
  successful status and the end of processing. A valid report without
  the register packing panel still passes;
- Quartus metastability report: as many complete chains as announced;
- Quartus exceptions and ignored-constraints reports: recognized
  content, summary panel closed;
- Vivado: an open design with primitive cells before any netlist
  check, and recognized report_exceptions strings;
- every case: lm_checks.txt reports every check the case defines.

New negative cases synthesize a real design, then empty or truncate
the Quartus map report, close the Vivado design, or empty the Vivado
ignored-exceptions report (test_corrupt), and pass only if the run fails
with the expected LM_ERROR (expect_error).
- The six CDC sources state next to the Synplify Pro / Lattice LSE
  attributes, and in their headers, that these attributes (also read by
  Synplify Pro for Microchip Libero) are not verified, as the user guide
  does: a single copied unit carries the information.
- async_reset and clock_measure headers, and the user guide: the false
  path on the CLR/clrn pins removes the asynchronous reset on those pins
  from timing analysis, release included, so no recovery/removal check
  is made there. The integrator remains responsible for the synchronized
  release and for verifying the downstream use of the synchronized reset.
- f_is_power_of_two has a single return statement (coding standard
  R20), with the same results; tb_vu_lm_util_pkg checks powers of two up
  to 2**30, their neighbours, 0 and negative values.
check_elab_failures.py printed the output of failing cases only, and an
intermittent local failure could not be traced back to its case. Each
case now writes its name, verdict, expectation, return code, start time,
duration, work directory, command and full GHDL output to
sim/work/elab_checks/<timestamp>/ (ignored by git; --log-dir overrides
it), with a summary.txt per run.
check_elab_failures.py failed intermittently on Windows (about one run
in five to ten under load): a case that followed a case of the same
entity failed at link time with "collect2.exe: error: ld returned 5
exit status" (also 1) and "compilation error", before any VHDL was
elaborated. GHDL links <entity>.exe in the work directory shared by all
cases, so consecutive cases of an entity relinked the executable the
previous case had just run, which can still be held open for a moment
(for instance by an on-access scan). Holding that file open reproduces
the same error on demand, and the same command with another output name
succeeds.

Each case now links its own executable (-o elab_case_<n>). The cases
still run one after the other; the change only removes the shared
output file.
The flushes are longer than the zero-delay testbench needs (it still
passes with C_TGL_FLUSH = 2 and C_RDY_FLUSH = 1). The extra stages are
the margin for a metastable first synchronizer stage after the reset,
which simulation cannot show; the comment next to the constants says so,
so that a later change does not shorten them on the strength of the
tests.
The fraction digits were round(frac * 10**precision), and the product
is itself rounded to the nearest real. When the exact product is just
below a decimal half, the rounded product can be that half, and the
result was rounded up a second time: f_string_format(0.0065, 3) gave
"0.007". 0.0065 is the binary64 value
0.006499999999999999701627562131989179761148989200592041015625, which rounds to
"0.006" at precision 3. Its rounded binary64 product with 1000 is exactly 6.5.
Against the final reference vectors, the previous body fails 22/1500 and 393/20000
cases; this fix gives zero mismatches.

The fraction v_int..C_VAL is exact (Sterbenz); Dekker's product now
gives the rounding error of frac * 10**precision exactly, and only a
remainder of exactly 0.5 uses its sign. Exact halves still round away
from zero, the carry and the range guards are unchanged.
Contract, in the function header, the user guide and the changelog: the
exact binary value of the real is rounded to precision digits, exact
halves away from zero (C printf rounds them to even). A decimal literal
is first the nearest real, so 9.9995 with precision 3 gives "9.999": the
real is 9.99949999999999938893..., and the "10.000" of e9e2ca4 came from
rounding the scaled product a second time.

sim/scripts/string_format_vectors.py writes reference vectors with a
fixed seed: the expected string is Decimal(float) quantized with
ROUND_HALF_UP, and each value is stored as sign, 53-bit mantissa and
binary exponent so that the testbench rebuilds the same real without
parsing a literal. The vectors cover random values over 19 orders of
magnitude, both signs, precision 0 to 9, doubles next to decimal halves,
exact halves (odd / 2**(precision + 1)), carries that add a digit and
the reviewed cases. run.py writes 1500 vectors for the fast and 20000
for the full regression to sim/work; the testbench compares every
string and counts the mismatches.

Mismatches with these vectors: 0 with the current function; 22/1500
and 393/20000 with the body of 18726a8 (before 3a844b5, which was
measured on an earlier set without the five double-rounding cases added
to the fixed list); 775/1500 and 8902/20000 with the body of e9e2ca4;
250/1500 and 2708/20000 with the body of 5d53232.
A real neg_shift_register map report whose "Registers Packed Into
Inferred Megafunctions" panel kept only its title and border (table of
contents, summary and end of processing intact) passed
no_shift_register with shift_taps_registers=0: the panel was found and
its rows were never validated.

A table panel that a check reads must now be complete when present, as
Quartus Prime Lite 23.1std writes it: border, expected column header,
border, at least one data row with as many cells as the header, closing
border. Quartus omits a table panel without rows, so a present panel
without rows is incomplete too. The rule covers every panel the checks
read: register packing (no_shift_register), Report Exceptions Summary
(constraints:listed and :valid), the Statistics table of each
metastability chain (chain:*) and the Analysis & Synthesis Summary
property table (status). A valid report without the packing panel
still passes.

New negative cases gut the packing panel of a real report: title and
border only, column header only, no closing border; each fails with
LM_ERROR. The README and the user guide state what the completeness
check guarantees (damaged, truncated or unrecognized reports fail) and
what it does not (a report edited to stay well formed passes).
rpt_table() returns no rows for an absent panel, and the metastability
validator only looked at the error: with the "Statistics" title row
removed from the real ccd_resync_2ff report, the parser still found the
method and the registers in the remaining rows and the case passed with
8 structural checks.

Each caller now handles an absent panel explicitly. Only the register
packing panel may be absent (Quartus omits it, and its table of
contents entry, when nothing is packed); a missing chain Statistics
panel or Analysis & Synthesis Summary is an LM_ERROR, and the exceptions
summary already was one unless the report states that no exceptions
were found. The unused rpt_panel() helper is removed.

Borders are recognized on the whole line, as Quartus 23.1 writes them
(runs of '-' between '+'), and every border and row of a table must have
its separators in the same columns; "+INVALID BORDER" is rejected. All
141 table panels of the existing real reports follow that format.

New negative cases: the chain Statistics panel without its title row,
and the packing panel with an invalid closing border; both fail with
LM_ERROR. On copies of all 89 real results with structural checks the
verdicts of this runner and of the e05a0e0 runner are identical.
With a value such as -1.0e300 and precision 9, the product of the
fraction and 10**precision overflowed and GHDL stopped on a bound check
failure inside the function, before the range assert was reached: the
input was rejected, but not by the explicit check.

The input checks now come first. A magnitude of 2**31 or more is
rejected by the range assert before any intermediate computation, and
an input out of range is not computed further when a tool continues
after the assert; the assert after the carry still rejects a value that
rounds up past integer'high. The behavior on valid inputs is unchanged:
0 mismatches on the 1500 fast and 20000 full reference vectors.

The function header and the user guide state the numeric assumptions of
the exact rounding: IEEE 754 binary64 reals with round to nearest, every
intermediate operation rounded on its own (no fused multiply-add, no
reassociation, no extended precision). The VHDL standard does not
require binary64 in every case; the profile verified is GHDL 6.0.0 with
the LLVM backend, Vivado Simulator and Questa/ModelSim are not.

New elaboration checks: 1.0e300 and -1.0e300, and carries just above
integer'high (2147483647.5 with precision 0, 2147483647.9995 with
precision 3); the reference vectors add the values just inside the
range.
@aca-logimentor
aca-logimentor merged commit b5a6a7c into main Oct 18, 2026
4 checks passed
@aca-logimentor
aca-logimentor deleted the bugfix/rtl-audit-fixes branch October 18, 2026 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants